import web, time, re, sys, time
from semantic.lsa import LSA
from semantic.rav import rav
from data.abstracts import abstracts

# web server
urls = (
   '/stop', 'stop',
   '/', 'index'
)
#app = web.application(urls, globals(), True)
render = web.template.render('templates/')
query_form = web.form.Form(
    web.form.Textbox('QueryClass'),
    web.form.Textarea('QueryString',
                      web.form.notnull,
                      rows=15, cols=60,
                      description="QUERY ",))

class PERFECTDatastore:
   """Keeps items forever; ignores hints"""
   def __init__(self):
      self.data = {}
   def add(self, itm, result):
      """Add itm paired with result to datastore. Probably best to remove stale
      entries in store here"""
      self.data[itm] = result
   def get(self, itm):
      """Retreive itm"""
      return self.data[itm]
   def hint(self, itm, similar):
      """Update state of cache, given the current query item (itm) and a list 
      of (itm, similarity_score). This can be used to update time codes and
      help control what gets removed from the store and when"""
      pass
   def check(self, itm):
      """Return True if itm is in cache, False otherwise"""
      return itm in self.data

class FIFODatastore(PERFECTDatastore):
   """Keeps items up to limit; discards in FIFO order; ignores hints"""
   def __init__(self):
      self.data = []
      self.max = 100
   def add(self, itm, result):
      """Add itm paired with result to datastore."""
      self.data.append( (itm, result) )
      # should we drop one?
      if len(self.data)>self.max:
         self.data.pop(0)
   def get(self, itm):
      """Retreive itm"""
      for cached_itm, result in self.data:
         if itm==cached_itm:
            return result
      raise KeyError(itm)
   def check(self, itm):
      """Return True if itm is in cache, False otherwise"""
      for cached_itm, result in self.data:
         if itm==cached_itm:
            return True
      return False

class LRUDatastore(PERFECTDatastore):
   """Keeps items up to limit; discards in LRU order; ignores hints"""
   def __init__(self):
      self.data = {}
      self.max = 100
   def add(self, itm, result):
      """Add itm paired with result to datastore."""
      self.data[itm] = [time.time(), result]
      # should we drop one?
      if len(self.data)>self.max:
         oldest_t = sys.float_info.max
         oldest_itm = None
         for itm, info in self.data.items():
            t, result = info
            if t<oldest_t:
               oldest_t = t
               oldest_itm = itm
         if oldest_itm:
            del self.data[oldest_itm]
            self.deleted(oldest_itm)
   def deleted(self, itm):
      pass
   def hint(self, itm, similar):
      """Update state of cache, given the current query item (itm)."""
      if itm in self.data:
         self.data[itm][0] = time.time()
   def get(self, itm):
      """Retreive itm"""
      return self.data[itm][1]

class LSADatastore(LRUDatastore):
   """Keeps items up to limit; discards by LRU; uses hints to update time"""
   def hint(self, itm, similar):
      """Update state of cache, given the current query item (itm) and a list 
      of (itm, similarity_score). This can be used to update time codes and
      help control what gets removed from the store and when"""
      for itm, score in similar:
         if itm in self.data:
            self.data[itm][0] = time.time()
   def deleted(self, itm):
      if self.lsa.delete('#%s' % itm)==-1:
         sys.stderr.write("error deleting rav\n")

# class for caching, lsa
class Cache:
   NUM_LOOKUP = 16
   MIN_SIMILARITY = .2
   def __init__(self, datastore_cls):
      self.dscls = datastore_cls
      self.store = datastore_cls()
      self.lsa = LSA()
      self.store.lsa = self.lsa
      self.f = open('data/short_abstracts_en.nt', 'r', 1)
      self.r = re.compile(r'<[^>]+>\s*<[^>]+>\s*"(.+?)"@en \.')
      self.hits = 0
      self.misses = 0
      self.DEBUG = False
   def getAbstractForItem(self, itm):
      try:
         offset = abstracts[itm]
         self.f.seek(offset)
         line = self.f.readline().strip()
         m = self.r.match(line)
         if m:
            txt = m.group(1)
         else:
            if self.DEBUG: print 'ERROR: failed to get abstract from', line
            txt = '-'
      except KeyError:
         if self.DEBUG: print 'ERROR: failed to get offset for', itm
         txt = '-'
      return txt
   def query(self, class_, itm):
      lower_itm = itm.encode('ascii','replace').lower()
      # now check to see if we already have it cached
      key = lower_itm
      if self.store.check(key):
         self.hits += 1
         if self.DEBUG: print "found", itm, "in cache - quick retrieval"
         result = self.store.get(key)
      else:
         self.misses += 1
         if self.DEBUG: print "query NOT found in cache - will query then add..."
         result = self.getAbstractForItem(itm)
         self.store.add(key, result)
         self.lsa.add_text(result)
         self.addInstance(lower_itm)
      # use item to give hints to datastore
      self.updateDatastore(lower_itm)
      return result
   def updateDatastore(self, inst):
      name = '#%s' % inst
      id = self.lsa.lookup(name)
      if id!=-1:
         similar = [(nm[1:],score) for type,idx,nm,score in \
          self.lsa.find_closest(name, self.NUM_LOOKUP, already_a_word=True) \
          if idx!=-1 and type==rav.RAV_VALUE and score>=self.MIN_SIMILARITY]
         self.store.hint(inst, similar)
   def addInstance(self, inst):
      name = '#%s' % inst
      # add representation for instance
      id = self.lsa.lookup(name)
      if id==-1:
         id = self.lsa.new(name)
      words = inst.split("_")
      wids = self.lsa.get_ids(words)
      self.lsa.accumulate_env(wids, id)
   def report(self):
      print '("%s", %d, %d, %d, %3.2f),' % \
       (self.dscls.__name__, self.misses, self.hits, \
       self.hits+self.misses, self.hits/float(self.misses+self.hits))
   def save(self):
      self.lsa.normalize()
      self.lsa.save('test.rav')

if len(sys.argv)>1:
   if sys.argv[1]=='LRU':
      theCache = Cache(LRUDatastore)
   elif sys.argv[1]=='FIFO':
      theCache = Cache(FIFODatastore)
   elif sys.argv[1]=='LSA':
      theCache = Cache(LSADatastore)
   else:
      theCache = Cache(PERFECTDatastore)
   sys.argv = sys.argv[:1] # web py shouldn't be looking at this by itself, tsk tsk
else:
   theCache = Cache(PERFECTDatastore)

# web page for query
class index:
    def GET(self):
        my_form = query_form()
        return render.hello(my_form)

    def POST(self):
        my_form = query_form() 
        if not my_form.validates(): 
            return render.hello(my_form)
        else:
            class_ = my_form['QueryClass'].value
            string = my_form['QueryString'].value
        qtime = time.time() 
        qresult = theCache.query(class_, string)
        qtime = time.time() - qtime
        #print "took %f" % qtime
        return qresult

class stop:
    def GET(self):
        #print "stopping"
        theCache.report()
        theCache.save()
        raise SystemExit
 
if __name__ == "__main__":
   app = web.application(urls, {'stop':stop,'index':index}, True)
   web.internalerror = web.debugerror
   app.run()
